package org.aceor.mddal.gateway.router;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.*;
import org.aceor.mddal.common.sharding.ShardingFunction;
import org.aceor.mddal.common.sqlparser.result.SQLBuilder;
import org.aceor.mddal.common.sqlparser.result.condition.ComparableAND;
import org.aceor.mddal.common.sqlparser.result.condition.ComparableOR;
import org.aceor.mddal.common.sqlparser.result.condition.ComparableUnit;
import org.aceor.mddal.common.sqlparser.result.context.ShardingContext;
import org.aceor.mddal.common.sqlparser.result.object.Column;
import org.aceor.mddal.common.utils.DateUtil;
import org.aceor.mddal.gateway.executor.ExecuteContext;
import org.apache.commons.collections.CollectionUtils;
import org.joda.time.DateTime;

import java.sql.Date;
import java.sql.Timestamp;
import java.util.*;

/**
 * Created by lxue on 16/4/12.
 */
public class RouteHelper {
    public static Set<Object> calAcceptableValues(ComparableUnit comparableUnit, ShardingFunction shardingFunction) {
        Set<Object> resultSet;
        if (comparableUnit instanceof ComparableAND) {
            resultSet = calAcceptableValuesByAND(comparableUnit, shardingFunction);
        } else if (comparableUnit instanceof ComparableOR) {
            resultSet = calAcceptableValuesByOR(comparableUnit, shardingFunction);
        } else {
            resultSet = calAcceptableValuesByCommon(comparableUnit, shardingFunction);
        }
        return resultSet;
    }

    private static Set<Object> calAcceptableValuesByAND(ComparableUnit comparableUnit, ShardingFunction shardingFunction) {
        ComparableAND and = (ComparableAND) comparableUnit;
        Set<Object> left = calAcceptableValues(and.getComparableUnion().get(0), shardingFunction);
        Set<Object> right = calAcceptableValues(and.getComparableUnion().get(1), shardingFunction);
        return Sets.intersection(left, right);
    }

    private static Set<Object> calAcceptableValuesByOR(ComparableUnit comparableUnit, ShardingFunction shardingFunction) {
        ComparableOR or = (ComparableOR) comparableUnit;
        Set<Object> result = calAcceptableValues(or.getComparableUnion().get(0), shardingFunction);
        for (int i = 1; i < or.getComparableUnion().size(); i++) {
            result = Sets.union(result, calAcceptableValues(or.getComparableUnion().get(i), shardingFunction));
        }
        return result;
    }

    private static Set<Object> calAcceptableValuesByCommon(ComparableUnit comparableUnit, ShardingFunction shardingFunction) {
        boolean isAdd;
        boolean skipBoundary;
        switch (comparableUnit.getBinaryOperator()) {
            case EQUIVALENT:
                return calAcceptableValuesByCommonInternal(comparableUnit.getValue());
            case GREATER_THAN:
                isAdd = true;
                skipBoundary = true;
                break;
            case GREATER_THAN_OR_EQUAL:
                isAdd = true;
                skipBoundary = false;
                break;
            case LESS_THAN:
                isAdd = false;
                skipBoundary = true;
                break;
            case LESS_THAN_OR_EQUAL:
                isAdd = false;
                skipBoundary = false;
                break;
            case NOT_EQUIVALENT:
            case LIKE:
            case NOT_LIKE:
            default:
                throw new UnsupportedOperationException("不支持类型: " + comparableUnit.getBinaryOperator());
        }
        return calAcceptableValuesByCommonInternal(comparableUnit.getValue()
                , shardingFunction.getStep()
                , shardingFunction.getMaxStepCounts()
                , shardingFunction.getLinearMeasure()
                , isAdd, skipBoundary);
    }

    /**
     * 等号处理
     *
     * @param value
     * @return
     */
    private static Set<Object> calAcceptableValuesByCommonInternal(Comparable value) {
        if (value instanceof Long) {
            return Sets.newHashSet((Long) value);
        } else if (value instanceof Integer) {
            return Sets.newHashSet(Long.valueOf((Integer) value));
        } else if (value instanceof String) {
            if (DateUtil.isTypeOfTimestamp((String) value)) {
                return Sets.newHashSet(Timestamp.valueOf(removeQuotes((String) value)));
            } else {
                return Sets.newHashSet(Date.valueOf(removeQuotes((String) value)));
            }
        } else {
            throw new IllegalArgumentException("不支持非Long和Integer以及String(Time)的数据类型 :" + value);
        }
    }

    private static String removeQuotes(String value) {
        if (value.contains("\'")) {
            return value.replace('\'', ' ').trim();
        }
        return value;
    }

    /**
     * @param initValue
     * @param step
     * @param maxStepTimes
     * @param linearMeasure
     * @param skipBoundary  是否跳过边界
     * @return
     */
    private static Set<Object> calAcceptableValuesByCommonInternal(Object initValue, Number step, int maxStepTimes, ShardingFunction.LinearMeasure linearMeasure, boolean isAdd, boolean skipBoundary) {
        Preconditions.checkArgument(step instanceof Long || step instanceof Integer, "步长暂时只支持Integer和Long");
        Preconditions.checkArgument(maxStepTimes > 0, "最大步长增长次数必须大于0");
        Preconditions.checkArgument(initValue instanceof Long || initValue instanceof Integer || initValue instanceof String, "步长暂时只支持Integer和Long和String(时间类型)");
        Long stepLong;
        if (step instanceof Long) {
            stepLong = (Long) step;
        } else {
            stepLong = Long.valueOf((Integer) step);
        }
        Set<Object> values = Sets.newHashSet();
        int start = skipBoundary ? 1 : 0;
        for (int i = start; i <= maxStepTimes; i++) {
            values.add(doAddOrMinus(initValue, stepLong, i, linearMeasure, isAdd));
        }
        return values;
    }

    /**
     * @param initValue     初始值
     * @param stepLong      步长
     * @param times
     * @param linearMeasure 单位
     * @param isAdd         是否是加法
     * @return
     */
    private static Object doAddOrMinus(Object initValue, Long stepLong, int times, ShardingFunction.LinearMeasure linearMeasure, boolean isAdd) {
        if (initValue instanceof String) {
            //默认当成时间单位处理
            boolean isTypeOfTimestamp = DateUtil.isTypeOfTimestamp((String) initValue);
            DateTime currentTime = new DateTime(DateUtil.getTime(removeQuotes((String) initValue)));
            currentTime = doAddOrMinusByTime(currentTime, stepLong, times, linearMeasure, isAdd);
            //TODO: 这里需要考虑返回String还是时间对象
            if (isTypeOfTimestamp) {
                return new Timestamp(currentTime.getMillis());
            } else {
                return new Date(currentTime.getMillis());
            }
        } else if (initValue instanceof Long) {
            if (isAdd) {
                return (Long) initValue + stepLong * times;
            } else {
                return (Long) initValue - stepLong * times;
            }
        } else {
            if (isAdd) {
                return Long.valueOf((Integer) initValue) + stepLong * times;
            } else {
                return Long.valueOf((Integer) initValue) - stepLong * times;
            }
        }
    }

    private static DateTime doAddOrMinusByTime(DateTime currentTime, Long stepLong, int times, ShardingFunction.LinearMeasure linearMeasure, boolean isAdd) {
        switch (linearMeasure) {
            case SECOND:
                if (isAdd) {
                    return currentTime.plusSeconds(stepLong.intValue() * times);
                } else {
                    return currentTime.minusSeconds(stepLong.intValue() * times);
                }
            case MINUTE:
                if (isAdd) {
                    return currentTime.plusMinutes(stepLong.intValue() * times);
                } else {
                    return currentTime.minusMinutes(stepLong.intValue() * times);
                }
            case HOUR:
                if (isAdd) {
                    return currentTime.plusHours(stepLong.intValue() * times);
                } else {
                    return currentTime.minusHours(stepLong.intValue() * times);
                }
            case DAY:
                if (isAdd) {
                    return currentTime.plusDays(stepLong.intValue() * times);
                } else {
                    return currentTime.minusDays(stepLong.intValue() * times);
                }
            case MOUTH:
                if (isAdd) {
                    return currentTime.plusMonths(stepLong.intValue() * times);
                } else {
                    return currentTime.minusMonths(stepLong.intValue() * times);
                }
            case YEAR:
                if (isAdd) {
                    return currentTime.plusYears(stepLong.intValue() * times);
                } else {
                    return currentTime.minusYears(stepLong.intValue() * times);
                }
            default:
                throw new IllegalArgumentException("不支持类型 :" + linearMeasure + ",仅支持秒,分,时,天,月,年");
        }
    }

    /**
     * @param comparableUnitList
     * @param shardingFunction
     * @return
     */
    public static Set<Object> getAcceptableValues(List<ComparableUnit> comparableUnitList, ShardingFunction shardingFunction) {
        if (CollectionUtils.isNotEmpty(comparableUnitList)) {
            Set<Object> values = Sets.newHashSet();
            boolean first = true;
            for (ComparableUnit comparableUnit : comparableUnitList) {
                if (first) {
                    values.addAll(RouteHelper.calAcceptableValues(comparableUnit.fillValue(), shardingFunction));
                    first = false;
                } else {
                    values = Sets.intersection(RouteHelper.calAcceptableValues(comparableUnit.fillValue(), shardingFunction), values);
                }
            }
//            forEachPrint(values);
            return values;
        } else {
            return Collections.emptySet();
        }
    }

    /**
     * @param executeContext  执行上下文
     * @param column
     * @param values          可取参数
     * @param shardingContext 分库分表上下文
     * @param sqlBuilder      sql输出器
     * @param args            参数值,给?用的
     * @param defaultDbIndex  不分库时默认的groupDbKey
     */
    public static void setExecuteSqlMapWhenSingleTbOrDbSharding(ExecuteContext executeContext, Column column, Set<Object> values, ShardingContext shardingContext, SQLBuilder sqlBuilder, List<Object> args, String defaultDbIndex) {
        if (Strings.isNullOrEmpty(defaultDbIndex)) {
            Multimap<String, Object> multimap = dbShardingInternal(executeContext,
                    values,
                    column,
                    shardingContext,
                    sqlBuilder,
                    args);
            for (String groupDsKey : multimap.keySet()) {
                //只放一个IN合并参数的RealSqlContext
                String sql = sqlBuilder.replaceWhereAppendToken(buildINClause(column, multimap.get(groupDsKey))).toSQL();
                executeContext.sqlMap.put(groupDsKey, Arrays.asList(new RealSqlContext(sql, shardingContext.logicTableName, args)));
            }
        } else {
            //只分表
            executeContext.sqlMap.put(defaultDbIndex, tbShardingInternal(values, column, shardingContext, sqlBuilder, args));
        }
    }

    /**
     * 构建 sharingColumn IN (value1,value2...) ==> 替换sqlBuilder中的whereAppendToken
     *
     * @param column
     * @param values
     * @return
     */
    private static String buildINClause(Column column, Collection<Object> values) {
        if (CollectionUtils.isEmpty(values)) {
            throw new NullPointerException("不存在可取值");
        }
        //对时间进行特殊处理
        Collection<Object> newValues = Collections2.transform(values, new Function<Object, Object>() {
            @Override
            public Object apply(Object input) {
                if (input instanceof Date || input instanceof Timestamp) {
                    //时间类型
                    return new StringBuilder("'").append(input).append("'").toString();
                }
                return input;
            }
        });
        StringBuilder sb = new StringBuilder();
        sb.append(column.toString())
                .append(" IN (")
                .append(Joiner.on(",").join(newValues))
                .append(") ");
        return sb.toString();
    }

    public static void setExecuteSqlMapWhenTbAndDbSameSharding(ExecuteContext executeContext, Column column, Set<Object> values, ShardingContext shardingContext, SQLBuilder sqlBuilder, List<Object> args) {
        Multimap<String, Object> multimap = dbShardingInternal(executeContext,
                values,
                column,
                shardingContext,
                sqlBuilder,
                args);
        for (String groupDbKey : multimap.keySet()) {
            executeContext.sqlMap.put(groupDbKey, tbShardingInternal(multimap.get(groupDbKey), column, shardingContext, sqlBuilder, args));
        }
    }

    /**
     * 分库的公共处理部分,没有分表做的优雅
     *
     * @param executeContext
     * @param values
     * @param column
     * @param shardingContext
     * @param sqlBuilder
     * @param args
     * @return
     */
    private static Multimap<String, Object> dbShardingInternal(ExecuteContext executeContext, Collection<Object> values, Column column, ShardingContext shardingContext, SQLBuilder sqlBuilder, List<Object> args) {
        Multimap<String, Object> multimap = HashMultimap.create();
        for (Object value : values) {
            int segmentCode = shardingContext.dbShardingFunction.executeSharding(value);
            String groupDsKey = shardingContext.convertDbName(segmentCode);
            multimap.put(groupDsKey, value);
        }
        return multimap;
    }

    /**
     * 分表的公共处理部分
     *
     * @param values
     * @param column
     * @param shardingContext
     * @param sqlBuilder
     * @param args
     * @return
     */
    private static List<RealSqlContext> tbShardingInternal(Collection<Object> values, Column column, ShardingContext shardingContext, SQLBuilder sqlBuilder, List<Object> args) {
        /**
         * key : 计算出的分表编号
         * value : 字段原值
         */
        Multimap<Integer, Object> multimap = HashMultimap.create();
        for (Object value : values) {
            int segmentCode = shardingContext.tbShardingFunction.executeSharding(value);
            multimap.put(segmentCode, value);
        }
        List<RealSqlContext> realSqlContextList = Lists.newLinkedList();
        for (Integer segmengCode : multimap.keySet()) {
            //生成实际表名
            String realTableName = shardingContext.convertTbName(segmengCode);
            column.getTable().setName(realTableName);
            //替换表名+添加分表字段In值表达式
            String sql = sqlBuilder.replaceToken(shardingContext.logicTableName, realTableName)
                    .replaceWhereAppendToken(buildINClause(column, multimap.get(segmengCode)))
                    .toSQL();
            realSqlContextList.add(new RealSqlContext(sql, realTableName, args));
        }
        return realSqlContextList;
    }

    public static void setExecuteSqlMapWhenTbAndDbNotSameSharding(ExecuteContext executeContext, Column tbColumn, Column dbColumn, Set<Object> tbValues, Set<Object> dBValues, ShardingContext shardingContext, SQLBuilder sqlBuilder, List<Object> args) {
        //两层for循环而已,互不依赖而已
        Multimap<String, Object> dbMultimap = dbShardingInternal(executeContext,
                dBValues,
                dbColumn,
                shardingContext,
                sqlBuilder,
                args);
        for (String groupDbKey : dbMultimap.keySet()) {
            executeContext.sqlMap.put(groupDbKey, tbShardingInternal(tbValues, tbColumn, shardingContext, sqlBuilder, args));
        }
    }
}
